//
// JavaKnob
//
// Copyright (C) jextra.net.
//
//  This file is part of the Crack build system.
//
//  The Crack build system is free software; you can redistribute it 
//  and/or modify it under the terms of the GNU Lesser General Public
//  License as published by the Free Software Foundation; either
//  version 2.1 of the License, or (at your option) any later version.
//
//  The Crack build system is distributed in the hope that it will be 
//  useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//  Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public
//  License along with the Crack build system; if not, write to the Free
//  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
//  02111-1307 USA.
//

package net.jextra.crack.java;

import java.io.*;
import java.util.*;
import java.util.jar.*;
import net.jextra.crack.*;
import net.jextra.crack.tool.*;
import net.jextra.crack.type.*;

/**
 * <p>
 * This is a default implementation of build commands to compile java code.
 * </p>
 * <p>
 * <ol>
 * <li>clean
 * <li>compile
 * <li>version
 * <li>build
 * <li>javadoc
 * </ol>
 * <ol>
 * <li>cleanBuild
 * <li>buildFull
 * </ol>
 * </p>
 */
public class JavaKnob
{
    // ============================================================
    // Fields
    // ============================================================

    private JavaKnobParams params;

    private boolean shrinkwrapRun;
    private Collection<String> sourcePaths;

    // ============================================================
    // Constructors
    // ============================================================

    public JavaKnob( JavaKnobParams params )
    {
        this.params = params;
    }

    // ============================================================
    // Methods
    // ============================================================

    // ----------
    // public
    // ----------

    @CrackCommand( description = "Delete .class files within this package." )
    public void clean()
        throws IOException
    {
        assert params.getClassesDir() != null : "getClassesDir can not be null.";
        assert params.getSourceFileSet() != null : "getSourceFileSet can not be null.";

        params.getClassesDir().mkdirs();

        for ( String path : getSourcePaths() )
        {
            File dir = new File( params.getClassesDir(), path );
            System.out.printf( "Cleaning directory [%s].\n", dir.getCanonicalPath() );
            FileTool.cleanDir( dir );
        }
    }

    @CrackCommand( description = "Copy resources into the .class directory." )
    public void resources()
        throws Exception
    {
        assert params.getResourceDirs() != null : "getResourceDirs can not be null.";

        FileCopier copier = new FileCopier();
        FileStrainer strainer = new RegexFileStrainer().exclude( ".*\\.java" ).exclude( ".*\\.crack" );

        for ( File fromDir : params.getResourceDirs() )
        {
            FileSet fileSet = new StrainedFileSet( fromDir, strainer );

            for ( File from : fileSet )
            {
                String name = FileTool.getRelativePath( fromDir, from );
                File to = new File( params.getClassesDir(), name );
                to.getParentFile().mkdirs();
                copier.copyFile( from, to, FileCopier.Type.older );
            }
        }
    }

    @CrackCommand( description = "Compile .java files into .class files." )
    public int compile()
        throws Exception
    {
        assert params.getSourceDirs() != null : "getSourceDirs can not return null.";
        assert !params.getSourceDirs().isEmpty() : "getSourceDirs can not be empty.";
        assert params.getClassesDir() != null : "getClassesDir can not return null.";

        params.getClassesDir().mkdirs();

        CrackBuild.getInstance().invokeKnobCommand( this, "resources" );

        // Add in the crack jars to the classpath.
        LinkedHashSet<File> deps = new LinkedHashSet<File>();
        Collection<File> files = params.getDependencies( "compile" );
        if ( files != null )
        {
            deps.addAll( files );
        }
        deps.addAll( CrackBuild.getInstance().getCrackContext().getCrackClasspath() );

        JavaCompiler compiler = new JavaCompiler();
        for ( File sourceDir : params.getSourceDirs() )
        {
            compiler.addSourceDir( sourceDir );
        }
        compiler.setOutputDir( params.getClassesDir() );
        compiler.addDependencies( deps );
        return compiler.compile( params.getSourceFileSet() );
    }

    @CrackCommand( description = "Output build name and version number for this component." )
    public void info()
        throws Exception
    {
        System.out.println( "  Build Name: " + params.getBuildName() );
        System.out.println( "     Version: " + params.getVersionNumber() );
    }

    @CrackCommand( description = "Delete build directory." )
    public void cleanBuild()
        throws IOException
    {
        File dir = params.getBuildDir();

        assert dir != null : "getBuildDir can not return null.";

        dir.mkdirs();

        System.out.printf( "Cleaning build directory [%s].\n", dir.getCanonicalPath() );
        FileTool.cleanDir( dir );
    }

    @CrackCommand( description = "Create javadoc from .java files." )
    public void javadoc()
        throws Exception
    {
        assert params.getSourceFileSet() != null : "getSourceFileSet can not return null.";
        assert params.getJavadocDir() != null : "getJavadocDir can not return null.";

        CrackBuild.getInstance().invokeKnobCommand( this, "compile" );

        params.getJavadocDir().mkdirs();

        JavadocTool javadoc = new JavadocTool();
        javadoc.setOutputDir( params.getJavadocDir() );
        javadoc.setBootClassPath( params.getClassesDir().getAbsolutePath() + File.pathSeparator + getClasspath() );
        javadoc.execute( params.getSourceFileSet() );
    }

    private String getClasspath()
        throws IOException
    {
        StringBuilder builder = new StringBuilder( System.getProperty( "java.home" ) + "/lib/rt.jar" );
        if ( params.getDependencies( "compile", "runtime" ) != null )
        {
            for ( File file : params.getDependencies( "compile", "runtime" ) )
            {
                // A null file is most likely from the file not actually existing.
                if ( file == null )
                {
                    continue;
                }

                if ( builder.length() > 0 )
                {
                    builder.append( File.pathSeparator );
                }

                builder.append( file.getCanonicalPath() );
            }
        }

        return builder.toString();
    }

    @CrackCommand( description = "Build .jar file and place in build directory." )
    public void build()
        throws Exception
    {
        assert params.getSourceDirs() != null : "getSourceDirs can not return null.";
        assert !params.getSourceDirs().isEmpty() : "getSourceDirs can not be empty.";
        assert params.getSourceFileSet() != null : "getSourceFileSet can not return null.";
        assert params.getClassesDir() != null : "getClassesDir can not return null.";
        assert params.getBuildName() != null : "getBuildName can not return null.";
        assert params.getBuildDir() != null : "getBuildDir can not return null.";

        params.getBuildDir().mkdirs();

        //
        // Create jar manifest.
        //
        Manifest man = new Manifest();
        man.getMainAttributes().put( Attributes.Name.MANIFEST_VERSION, "1.0" );
        man.getMainAttributes().put( Attributes.Name.IMPLEMENTATION_TITLE, params.getBuildName() );
        if ( params.getVersionNumber() != null )
        {
            man.getMainAttributes().put( Attributes.Name.IMPLEMENTATION_VERSION, params.getVersionNumber() );
        }

        File file = new File( params.getBuildDir(), params.getBuildName() + ".jar" );
        System.out.printf( "Creating jar [%s].\n", file.getCanonicalPath() );
        CrackJarFile jarFile = new CrackJarFile( file, man );

        //
        // Add .class files.
        //
        for ( String pkgPath : getSourcePaths() )
        {
            File dir = new File( params.getClassesDir().getAbsolutePath(), pkgPath );
            System.out.printf( "Adding classes in [%s] to jar.\n", dir.getCanonicalPath() );
            RegexFileStrainer strainer = new RegexFileStrainer( new SimpleDirStrainer( pkgPath ) ).include(
                ".*\\.class" ).exclude( "Build\\.class" );
            jarFile.addFiles( new StrainedFileSet( params.getClassesDir(), strainer ) );
        }

        //
        // Add resources from source dirs.
        //
        for ( File dir : params.getResourceDirs() )
        {
            System.out.println();
            RegexFileStrainer strainer = new RegexFileStrainer().exclude( ".*\\.java" ).exclude( ".*\\.crack" );
            jarFile.addFiles( new StrainedFileSet( dir, strainer ) );
        }

        jarFile.close();

        //
        // Create ${componentName}-sources.zip file.
        //
        File srcFile = new File( params.getBuildDir(), params.getBuildName() + "-sources.zip" );
        System.out.printf( "Creating -sources.zip file [%s].\n", srcFile.getCanonicalPath() );
        CrackJarFile srcJarFile = new CrackJarFile( srcFile );
        JavaPackageFinder finder = new JavaPackageFinder();
        for ( File javaFile : params.getSourceFileSet() )
        {
            finder.read( javaFile );
            srcJarFile.addFile( finder.getRootDir(), javaFile );
        }
        srcJarFile.close();

        //
        // Create ${componentName}-javadoc.zip file.
        //
        //        File javadocFile = new File( params.getBuildDir(), params.getBuildName() + "-javadoc.zip" );
        //        System.out.printf( "Creating -javadoc.zip file [%s].\n", srcFile.getCanonicalPath() );
        //        CrackJarFile javadocJarFile = new CrackJarFile( javadocFile );
        //        StrainedFileSet fileSet = new StrainedFileSet( params.getJavadocDir() );
        //        javadocJarFile.addFiles( fileSet );
        //        javadocJarFile.close();
    }

    @CrackCommand( visible = false,
        description = "Build .jar file that includes all of classes not just this components." )
    public void buildFull()
        throws Exception
    {
        assert params.getSourceDirs() != null : "getSourceDirs can not return null.";
        assert !params.getSourceDirs().isEmpty() : "getSourceDirs can not be empty.";
        assert params.getClassesDir() != null : "getClassesDir can not return null.";
        assert params.getSourceFileSet() != null : "getSourceFileSet can not return null.";
        assert params.getBuildName() != null : "getBuildName can not return null.";
        assert params.getBuildDir() != null : "getBuildDir can not return null.";

        CrackBuild.getInstance().invokeKnobCommand( this, "javadoc" );

        params.getBuildDir().mkdirs();

        //
        // Add all .class files.
        //
        System.out.printf( "Adding classes to jar.\n" );
        CrackJarFile jarFile = new CrackJarFile( new File( params.getBuildDir(), params.getBuildName() + ".full.jar" ) );
        RegexFileStrainer strainer = new RegexFileStrainer().include( ".*\\.class" ).exclude( "Build\\.class" );
        jarFile.addFiles( new StrainedFileSet( params.getClassesDir(), strainer ) );

        //
        // Add all resources from source path.
        //
        strainer = new RegexFileStrainer().exclude( ".*\\.java" ).exclude( ".*\\.crack" );
        for ( File sourceDir : params.getSourceDirs() )
        {
            System.out.printf( "Adding resources in dir [%s] to jar.\n", sourceDir.getAbsolutePath() );
            jarFile.addFiles( new StrainedFileSet( sourceDir, strainer ) );
        }

        jarFile.close();

        //
        // Create source jar.
        //
        strainer = new RegexFileStrainer().include( ".*\\.java" );
        CrackJarFile zipFile = new CrackJarFile(
            new File( params.getBuildDir(), params.getBuildName() + "-sources.zip" ) );
        for ( File sourceDir : params.getSourceDirs() )
        {
            zipFile.addFiles( new StrainedFileSet( sourceDir, strainer ) );
        }
        zipFile.close();
    }

    /**
     * This creates a "pathing jar" from the "build jar" plus all of the dependent libraries.
     */
    @CrackCommand( description = "Merge build jar and dependency libs and make a 'pathing jar'." )
    public void shrinkwrap()
        throws Throwable
    {
        assert params.getBuildName() != null : "getBuildName can not be null.";
        assert params.getSourceDirs() != null : "getSourceDirs can not return null.";
        assert !params.getSourceDirs().isEmpty() : "getSourceDirs can not be empty.";
        assert params.getClassesDir() != null : "getClassesDir can not be null.";
        assert params.getDependencies( "compile", "runtime" ) != null : "getDependencies can not be null.";
        assert params.getShrinkwrapDir() != null : "getShrinkwrapDir can not be null.";

        if ( shrinkwrapRun )
        {
            System.out.println( "shrinkwrap has already been run." );
            return;
        }
        shrinkwrapRun = true;

        CrackBuild.getInstance().invokeKnobCommand( this, "build" );

        params.getShrinkwrapDir().mkdirs();

        //
        // Copy all dependent jars.
        //
        FileCopier copier = new FileCopier();
        for ( File zipFile : params.getDependencies( "compile", "runtime" ) )
        {
            if ( zipFile.isFile() )
            {
                File to = new File( params.getShrinkwrapDir(), zipFile.getName() );
                copier.copyFile( zipFile, to );
            }
        }

        //
        // Create .classes.jar
        //
        CrackJarFile jarFile = new CrackJarFile( new File( params.getShrinkwrapDir(), params.getBuildName()
            + ".classes.jar" ) );

        //
        // Add .class files.
        //
        System.out.printf( "Adding classes in dir [%s] to jar.\n", params.getClassesDir().getAbsolutePath() );
        RegexFileStrainer strainer = new RegexFileStrainer().include( ".*\\.class" ).exclude( "Build\\.class" );
        jarFile.addFiles( new StrainedFileSet( params.getClassesDir(), strainer ) );

        //        for ( String pkgPath : getSourcePaths() )
        //        {
        //            System.out.printf( "Adding classes in package %s to jar.\n", pkgPath );
        //            RegexFileStrainer strainer = new RegexFileStrainer( new SimpleDirStrainer( pkgPath ) ).include(
        //                            ".*\\.class" ).exclude( "Build\\.class" );
        //            jarFile.addFiles( new StrainedFileSet( _params.getClassesDir(), strainer ) );
        //        }

        //
        // Add resources from source paths.
        //
        System.out.printf( "Adding resources in dir [%s] to jar.\n", params.getClassesDir().getAbsolutePath() );
        strainer = new RegexFileStrainer().exclude( ".*\\.class" );
        jarFile.addFiles( new StrainedFileSet( params.getClassesDir(), strainer ) );

        //        for ( File sourceDir : _params.getSourceDirs() )
        //        {
        //            for ( String pkgPath : getSourcePaths() )
        //            {
        //                System.out.printf( "Adding resources in dir [%s/%s] to jar.\n", sourceDir.getAbsolutePath(), pkgPath );
        //                RegexFileStrainer strainer = new RegexFileStrainer( new SimpleDirStrainer( pkgPath ) ).exclude(
        //                    ".*\\.java" ).exclude( ".*\\.crack" );
        //                jarFile.addFiles( new StrainedFileSet( sourceDir, strainer ) );
        //            }
        //        }

        jarFile.close();

        //
        // Copy resource into shrinkwrap directory.
        //
        // File from = new File( "environment.deploy.properties" );
        // copier.copyFile( from, new File( getShrinkwrapAppDir(), "environment.properties" ) );

        // from = new File( "splash.gif" );
        // File to = new File( getShrinkwrapAppDir(), from.getName() );
        // if ( from.exists() )
        // {
        // copier.copyFile( from, to );
        // }
        // else
        // {
        // InputStream in = getClass().getResourceAsStream( FILENAME_SPLASH_IMG );
        // copier.copyStream( in, to );
        // in.close();
        // }

        //
        // Create MANIFEST.MF and pathing jar.
        //
        File manFile = new File( params.getShrinkwrapDir(), "MANIFEST.MF" );
        PrintWriter writer = new PrintWriter( manFile );
        writer.println( "Manifest-Version: 1.0" );
        writer.print( "Class-Path:" );

        strainer = new RegexFileStrainer().include( ".*\\.(jar|JAR|zip|ZIP)" );
        for ( File file : strainer.getFiles( params.getShrinkwrapDir() ) )
        {
            writer.printf( " %s \n", file.getName() );
        }

        writer.close();

        FileInputStream manIn = new FileInputStream( manFile );
        Manifest man = new Manifest( manIn );
        jarFile = new CrackJarFile( new File( params.getShrinkwrapDir(), params.getBuildName() + ".path.jar" ), man );
        jarFile.close();
        manIn.close();
    }

    // ----------
    // private
    // ----------

    private Collection<String> getSourcePaths()
        throws IOException
    {
        if ( sourcePaths != null )
        {
            return sourcePaths;
        }

        sourcePaths = new HashSet<String>();
        JavaPackageFinder finder = new JavaPackageFinder();
        for ( File javaFile : params.getSourceFileSet() )
        {
            finder.read( javaFile );
            if ( finder.getPath() != null )
            {
                sourcePaths.add( finder.getPath() );
            }
        }

        return sourcePaths;
    }
}
